/*
    Copyright © 2010  Ahti Legonkov
    
    This file is part of j+-.
    
    j+- is free software: you can redistribute it and/or modify
    it under the terms of the GNU General Public License as published by
    the Free Software Foundation, either version 3 of the License, or
    (at your option) any later version.
    
    j+- is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU General Public License for more details.
    
    You should have received a copy of the GNU General Public License
    along with this program.  If not, see <http://www.gnu.org/licenses/>.
*/

package cx.ath.skyflyer.javapm.visitor;

import cx.ath.skyflyer.javapm.symbol.SymbolTable;
import cx.ath.skyflyer.javapm.util.MessageCollection;
import cx.ath.skyflyer.javapm.util.Ref;
import japa.parser.JavaParser;
import japa.parser.ParseException;
import japa.parser.ast.BlockComment;
import japa.parser.ast.CompilationUnit;
import japa.parser.ast.ImportDeclaration;
import japa.parser.ast.LineComment;
import japa.parser.ast.Node;
import japa.parser.ast.PackageDeclaration;
import japa.parser.ast.TypeParameter;
import japa.parser.ast.body.AnnotationDeclaration;
import japa.parser.ast.body.AnnotationMemberDeclaration;
import japa.parser.ast.body.BodyDeclaration;
import japa.parser.ast.body.ClassOrInterfaceDeclaration;
import japa.parser.ast.body.ConstructorDeclaration;
import japa.parser.ast.body.DestructorDeclaration;
import japa.parser.ast.body.EmptyMemberDeclaration;
import japa.parser.ast.body.EmptyTypeDeclaration;
import japa.parser.ast.body.EnumConstantDeclaration;
import japa.parser.ast.body.EnumDeclaration;
import japa.parser.ast.body.FieldDeclaration;
import japa.parser.ast.body.InitializerDeclaration;
import japa.parser.ast.body.JavadocComment;
import japa.parser.ast.body.MethodDeclaration;
import japa.parser.ast.body.ModifierSet;
import japa.parser.ast.body.Parameter;
import japa.parser.ast.body.TypeDeclaration;
import japa.parser.ast.body.VariableDeclarator;
import japa.parser.ast.body.VariableDeclaratorId;
import japa.parser.ast.expr.AnnotationExpr;
import japa.parser.ast.expr.ArrayAccessExpr;
import japa.parser.ast.expr.ArrayCreationExpr;
import japa.parser.ast.expr.ArrayInitializerExpr;
import japa.parser.ast.expr.AssignExpr;
import japa.parser.ast.expr.BinaryExpr;
import japa.parser.ast.expr.BooleanLiteralExpr;
import japa.parser.ast.expr.CastExpr;
import japa.parser.ast.expr.CharLiteralExpr;
import japa.parser.ast.expr.ClassExpr;
import japa.parser.ast.expr.ConditionalExpr;
import japa.parser.ast.expr.DoubleLiteralExpr;
import japa.parser.ast.expr.EnclosedExpr;
import japa.parser.ast.expr.Expression;
import japa.parser.ast.expr.FieldAccessExpr;
import japa.parser.ast.expr.InstanceOfExpr;
import japa.parser.ast.expr.IntegerLiteralExpr;
import japa.parser.ast.expr.IntegerLiteralMinValueExpr;
import japa.parser.ast.expr.LongLiteralExpr;
import japa.parser.ast.expr.LongLiteralMinValueExpr;
import japa.parser.ast.expr.MarkerAnnotationExpr;
import japa.parser.ast.expr.MemberValuePair;
import japa.parser.ast.expr.MethodCallExpr;
import japa.parser.ast.expr.NameExpr;
import japa.parser.ast.expr.NormalAnnotationExpr;
import japa.parser.ast.expr.NullLiteralExpr;
import japa.parser.ast.expr.ObjectCreationExpr;
import japa.parser.ast.expr.QualifiedNameExpr;
import japa.parser.ast.expr.SingleMemberAnnotationExpr;
import japa.parser.ast.expr.StringLiteralExpr;
import japa.parser.ast.expr.SuperExpr;
import japa.parser.ast.expr.ThisExpr;
import japa.parser.ast.expr.UnaryExpr;
import japa.parser.ast.expr.VariableDeclarationExpr;
import japa.parser.ast.stmt.AssertStmt;
import japa.parser.ast.stmt.BlockStmt;
import japa.parser.ast.stmt.BreakStmt;
import japa.parser.ast.stmt.CatchClause;
import japa.parser.ast.stmt.ContinueStmt;
import japa.parser.ast.stmt.DoStmt;
import japa.parser.ast.stmt.EmptyStmt;
import japa.parser.ast.stmt.ExplicitConstructorInvocationStmt;
import japa.parser.ast.stmt.ExpressionStmt;
import japa.parser.ast.stmt.ForStmt;
import japa.parser.ast.stmt.ForeachStmt;
import japa.parser.ast.stmt.IfStmt;
import japa.parser.ast.stmt.LabeledStmt;
import japa.parser.ast.stmt.ReturnStmt;
import japa.parser.ast.stmt.Statement;
import japa.parser.ast.stmt.SwitchEntryStmt;
import japa.parser.ast.stmt.SwitchStmt;
import japa.parser.ast.stmt.SynchronizedStmt;
import japa.parser.ast.stmt.ThrowStmt;
import japa.parser.ast.stmt.TryStmt;
import japa.parser.ast.stmt.TypeDeclarationStmt;
import japa.parser.ast.stmt.WhileStmt;
import japa.parser.ast.type.ClassOrInterfaceType;
import japa.parser.ast.type.PrimitiveType;
import japa.parser.ast.type.ReferenceType;
import japa.parser.ast.type.Type;
import japa.parser.ast.type.VoidType;
import japa.parser.ast.type.WildcardType;
import japa.parser.ast.visitor.GenericVisitorAdapter;
import java.io.File;
import java.io.IOException;
import java.util.LinkedList;
import java.util.List;

/**
 * Checks that there are no assignments to any of the fields in readonly
 * methods - all fields gain final modifier in readonly methods.
 * @author Ahti Legonkov
 */
public class CheckAssignmentVisitor extends GenericVisitorAdapter<Boolean, Ref<Type>> {

    private CompilationUnit compilationUnit;
    private ClassOrInterfaceDeclaration currentClassOrInterface;
    private MethodDeclaration currentMethod;
    private ConstructorDeclaration currentConstructor;
    private DestructorDeclaration currentDestructor;
    private VariableDeclarationExpr currentVariableDeclaration;
    private FieldDeclaration currentFieldDeclaration;
    private final String sourceRoot;
    private final String sourcePath;

    private final MessageCollection messages = new MessageCollection();

    public MessageCollection getMessages() {
        return messages;
    }

    //
    // Constructors
    //

    public CheckAssignmentVisitor(String sourceRoot, String sourcePath) {
        this.sourceRoot = sourceRoot;
        this.sourcePath = sourcePath;
    }


    //
    // Overrides
    //

    @Override
    public Boolean visit(AssignExpr n, Ref<Type> arg) {
        assert(n.getData() != null);
        assert(n.getData() instanceof SymbolTable);

        Ref<Type> targetType = new Ref();
        if (!n.getTarget().accept(this, targetType).booleanValue())
            return Boolean.FALSE;

        if (targetType.data == null) {
            messages.addError("Unable to determine target type", sourcePath,
                    n.getBeginLine(), n.getBeginColumn());
            return Boolean.FALSE;
        }

        if (ModifierSet.isAuto(targetType.data.getModifiers())) {
            if (currentConstructor != null) {
                if (!(n.getValue() instanceof ObjectCreationExpr)) {
                    messages.addError("'auto' field must be initialized with new object.", sourcePath,
                            n.getBeginLine(), n.getEndColumn());
                    return Boolean.FALSE;
                }
            }
            else if (currentMethod != null || currentDestructor != null) {
                messages.addError("Cannot assign to 'auto' variable.", sourcePath,
                        n.getBeginLine(), n.getEndColumn());
                return Boolean.FALSE;
            }
            else {
                // exactly one of currentMethod, currentConstructor, currentDestructor
                // must be not null.
                assert(false);
            }
        }
        else if (ModifierSet.isFinal(targetType.data.getModifiers())) {
            messages.addError("Cannot assign to final", sourcePath,
                    n.getBeginLine(), n.getBeginColumn());
            return Boolean.FALSE;
        }

        Ref<Type> sourceType = new Ref();
        if (!n.getValue().accept(this, sourceType).booleanValue())
            return Boolean.FALSE;

        if (sourceType.data == null) {
            if (n.getValue() instanceof NullLiteralExpr) {
                if (targetType.data instanceof PrimitiveType) {
                    messages.addError(
                            "Cannot convert 'null' to '"+targetType.data+"'.", sourcePath,
                            n.getBeginLine(), n.getBeginColumn());
                    return Boolean.FALSE;
                }
                return Boolean.TRUE;
            }
            else {
                messages.addError(
                        "Unable to determine source type", sourcePath,
                        n.getBeginLine(), n.getBeginColumn());
                return Boolean.FALSE;
            }
        }

        final int srcmod = sourceType.data.getModifiers();
        final int dstmod = targetType.data.getModifiers();
        
        if (targetType.data instanceof PrimitiveType) {
            // if target is readonly, then it behaves like final. in this case
            // the java compiler will catch it.
        }
        else if (!ModifierSet.isReadOnly(targetType.data.getModifiers())) {
            if (ModifierSet.isReadOnly(sourceType.data.getModifiers())) {
                messages.addError(
                        "Conversion loses 'readonly'", sourcePath,
                        n.getBeginLine(), n.getBeginColumn());
                return Boolean.FALSE;
            }
        }

        return Boolean.TRUE;
    }

    @Override
    public Boolean visit(BlockStmt n, Ref<Type> arg) {
        Boolean res = Boolean.TRUE;

        if (n.getStmts() != null) {
            for (Statement s : n.getStmts()) {
                Ref<Type> refType = new Ref();
                final Boolean b = s.accept(this, refType);
                if (!b.booleanValue())
                    res = Boolean.FALSE;
            }
        }

        return res;
    }

    /**
     * Returns type info in arg i.e PrimitiveType(Boolean).
     * @param n - AST node
     * @param arg - reference to Ref object.
     * @return Boolean.TRUE
     */
    @Override
    public Boolean visit(BooleanLiteralExpr n, Ref<Type> arg) {
        assert(arg != null);
        Ref<Type> type = arg;
        type.data = new PrimitiveType(PrimitiveType.Primitive.Boolean);
        return Boolean.TRUE;
    }

    @Override
    public Boolean visit(CastExpr n, Ref<Type> arg) {
        assert(arg != null);
        Ref<Type> refType = arg;
        refType.data = n.getType();

        // TODO: check if the expession type and cast type are compatible.
        // i.e. like trying to assign reference type to primitive type.

        //n.getType().accept(this, arg);
        //n.getExpr().accept(this, arg);

        return Boolean.TRUE;
    }

    /**
     * Returns type info in arg i.e PrimitiveType(Char).
     * @param n - AST node
     * @param arg - reference to Ref object.
     * @return Boolean.TRUE
     */
    @Override
    public Boolean visit(CharLiteralExpr n, Ref<Type> arg) {
        assert(arg != null);
        Ref<Type> type = arg;
        type.data = new PrimitiveType(PrimitiveType.Primitive.Char);
        return Boolean.TRUE;
    }

    /**
     * If the class or interface is a generic class declaration, a warning is issued and processing
     * of the declaration stops there.
     * @param n AST node
     * @param arg
     * @return 
     */
    @Override
    public Boolean visit(ClassOrInterfaceDeclaration n, Ref<Type> arg) {

        if (n.getTypeParameters() != null) {
            messages.addWarning2("Generics not supported.", sourcePath,
                    n.getBeginLine(), n.getBeginColumn());
            return Boolean.FALSE;
        }

        final ClassOrInterfaceDeclaration prevClassOrInterface = currentClassOrInterface;
        currentClassOrInterface = n;

        if (n.getExtends() != null) {
            final SymbolTable symbolTable = (SymbolTable)n.getData();
            for (ClassOrInterfaceType c : n.getExtends()) {
                final ImportDeclaration import_ = symbolTable.getImport(c.getName());
                if (import_ != null) {
                    if (!tryImportType(c.getName(), arg, import_, symbolTable))
                        return Boolean.FALSE;
                }

                
                // can I extend an enum?
                //EnumDeclaration enum_ = symbolTable.getEnum(base.getName());
                // ...


                // Now that the type is imported, the name should resolve to class declaration.
                final ClassOrInterfaceDeclaration cls = symbolTable.getClass(c.getName());
                if (cls == null)
                    // ignore it - the class may have been declared in jar or
                    // compiled class file.
                    continue;

                if (currentClassOrInterface.isInterface()) {
                    if (!cls.isInterface()) {
                        messages.addError("Interface expected, but found class '"+cls.getName()+"'.", sourcePath,
                                n.getBeginLine(), n.getBeginColumn());
                        return Boolean.FALSE;
                    }
                }
                else {
                    if (cls.isInterface()) {
                        messages.addError("Class expected, but found interface '"+cls.getName()+"'.", sourcePath,
                                n.getBeginLine(), n.getBeginColumn());
                        return Boolean.FALSE;
                    }
                }

                Ref<Type> baseType = new Ref();
                c.accept(this, baseType);
            }
        }

        if (n.getImplements() != null) {
            for (ClassOrInterfaceType c : n.getImplements()) {
                Ref<Type> baseType = new Ref();
                c.accept(this, baseType);
            }
        }
        if (n.getMembers() != null) {
            for (BodyDeclaration member : n.getMembers()) {
                Ref<Type> memberType = new Ref();
                member.accept(this, memberType);
            }
        }

        currentClassOrInterface = prevClassOrInterface;
        return Boolean.TRUE;
    }

    /**
     * Class, interface, enum, package
     * @param n
     * @param arg
     * @return
     */
    @Override
    public Boolean visit(ClassOrInterfaceType n, Ref<Type> arg) {

        if (n.getTypeArgs() != null) {
            messages.addWarning2(
                    "Generics not supported. Performing passthru.", sourcePath,
                    n.getBeginLine(), n.getBeginColumn());
            return Boolean.FALSE;
        }

        assert(arg != null);
        assert(n.getData() != null);
        assert(n.getData() instanceof SymbolTable);

        SymbolTable st = (SymbolTable)n.getData();

        if (n.getScope() != null) {
            Boolean res = n.getScope().accept(this, arg);
            if (!res.booleanValue())
                return res;

            st = (SymbolTable)n.getScope().getData();
        }
        else {
            st = (SymbolTable)n.getData();
        }

        //System.out.println("line:"+n.getBeginLine());
        Node cls = st.getClass(n.getName());
        Node enum_ = st.getEnum(n.getName());
        Node package_ = st.getPackage(n.getName());

        arg.data = n;

        return Boolean.TRUE;
    }

    @Override
    public Boolean visit(CompilationUnit n, Ref<Type> arg) {
        compilationUnit = n;
        super.visit(n, arg);
        return Boolean.TRUE;
    }

    @Override
    public Boolean visit(ConstructorDeclaration n, Ref<Type> arg) {

        if (n.getTypeParameters() != null) {
            messages.addWarning2(
                    "Generics not supported.", sourcePath,
                    n.getBeginLine(), n.getBeginColumn());
            return Boolean.FALSE;
        }

        final ConstructorDeclaration prevConstructor = currentConstructor;
        final DestructorDeclaration prevDestructor = currentDestructor;
        final MethodDeclaration prevMethod = currentMethod;

        currentConstructor = n;
        currentDestructor = null;
        currentMethod = null;

        Boolean res = Boolean.TRUE;
        if (n.getTypeParameters() != null) {
            messages.addWarning2(
                    "Generics not supported. Performing passthru.", sourcePath,
                    n.getBeginLine(), n.getBeginColumn());
            res = Boolean.FALSE;
        }

        if (res.booleanValue() && n.getAnnotations() != null) {
            messages.addWarning2(
                    "Ignoring annotations.", sourcePath,
                    n.getBeginLine(), n.getBeginColumn());
        }

        if (arg == null)
            arg = new Ref();
        
        if (n.getParameters() != null) {
            for (Parameter p : n.getParameters()) {
                p.accept(this, arg);
            }
        }

        if (res.booleanValue())
            res = n.getBlock().accept(this, arg);

        currentConstructor = prevConstructor;
        currentDestructor = prevDestructor;
        currentMethod = prevMethod;
        return res;
    }

    @Override
    public Boolean visit(DestructorDeclaration n, Ref<Type> arg) {
        final ConstructorDeclaration prevConstructor = currentConstructor;
        final DestructorDeclaration prevDestructor = currentDestructor;
        final MethodDeclaration prevMethod = currentMethod;

        currentConstructor = null;
        currentDestructor = n;
        currentMethod = null;

        Boolean res = super.visit(n, arg);

        currentConstructor = prevConstructor;
        currentDestructor = prevDestructor;
        currentMethod = prevMethod;
        return res;
    }

    /**
     * Returns type info in arg i.e PrimitiveType(Double).
     * @param n - AST node
     * @param arg - reference to Ref object.
     * @return Boolean.TRUE
     */
    @Override
    public Boolean visit(DoubleLiteralExpr n, Ref<Type> arg) {
        assert(arg != null);
        Ref<Type> type = arg;
        type.data = new PrimitiveType(PrimitiveType.Primitive.Double);
        return Boolean.TRUE;
    }

    @Override
    public Boolean visit(FieldAccessExpr n, Ref<Type> arg) {
        assert(arg != null);

        Ref<Type> scopeType = new Ref();
        Boolean res = n.getScope().accept(this, scopeType);
        if (!res.booleanValue())
            return res;

        assert(scopeType.data != null);
        
        if (scopeType.data instanceof ReferenceType) {
            ReferenceType refType = (ReferenceType)scopeType.data;
            if (refType.getArrayCount() > 0) {
                messages.addWarning1(
                        "Unable to find type '"+refType+"'. Performing passthru.", sourcePath,
                        n.getBeginLine(), n.getBeginColumn());
                return Boolean.FALSE;
            }
        }

        SymbolTable scopeSymbols = (SymbolTable)scopeType.data.getData();
        assert(scopeSymbols != null);

        // The field may be either EnumConstant or a Field.
        Node field = scopeSymbols.getField(n.getField());
        if (field != null) {
            arg.data = ((FieldDeclaration)field).getType();
        }
        else {
            field = scopeSymbols.getEnumConstant(n.getField());
            if (field != null) {
                arg.data = scopeType.data;
            }
        }

        if (field == null) {
            messages.addWarning4(
                    "Unable to find symbol '"+n.getField()+"' in '"+scopeType.data+"'. Performing passthru.", sourcePath,
                    n.getBeginLine(), n.getBeginColumn());
            return Boolean.FALSE;
        }
        
        return res;
    }

    @Override
    public Boolean visit(FieldDeclaration n, Ref<Type> arg) {
        final FieldDeclaration prevFieldDeclaration = currentFieldDeclaration;
        currentFieldDeclaration = n;
        Ref<Type> fieldType = new Ref();
        super.visit(n, fieldType);
        currentFieldDeclaration = prevFieldDeclaration;
        return Boolean.TRUE;
    }

    /**
     * Returns type info in arg i.e PrimitiveType(Int).
     * @param n - AST node
     * @param arg - reference to Ref object.
     * @return Boolean.TRUE
     */
    @Override
    public Boolean visit(IntegerLiteralExpr n, Ref<Type> arg) {
        assert(arg != null);
        Ref<Type> type = arg;
        type.data = new PrimitiveType(PrimitiveType.Primitive.Int);
        return Boolean.TRUE;
    }

    /**
     * Returns type info in arg i.e PrimitiveType(Long).
     * @param n - AST node
     * @param arg - reference to Ref object. The Ref object will reference to an instance
     * of PrimitiveType.
     * @return Boolean.TRUE
     */
    @Override
    public Boolean visit(LongLiteralExpr n, Ref<Type> arg) {
        assert(arg != null);
        Ref<Type> type = arg;
        type.data = new PrimitiveType(PrimitiveType.Primitive.Long);
        return Boolean.TRUE;
    }

    /**
     * Dereferences the reference type. If the reference type is an array, a warning is ussued and
     * null is returned.
     * @param n node in the AST. It's used to obtain the location information to embed in
     *  the warning message.
     * @param type a reference type to dereference.
     * @return the dereferenced type. null if array count of the reference type is greater than zero.
     */
    private Type dereferenceType(Node n, ReferenceType type) {
        if (type.getArrayCount() > 0) {
            messages.addWarning1(
                    "Unable to find type '"+type+"'. Performing passthru.", sourcePath,
                    n.getBeginLine(), n.getBeginColumn());
            return null;
        }

        final int mod = type.getModifiers();
        final Type resultType = type.getType().clone();
        resultType.setModifiers(mod);
        return resultType;
    }

    /**
     * 
     * @param n
     * @param scope
     * @param type
     * @return
     */
    private SymbolTable getSymbolTable(Node n, ClassOrInterfaceType type) {
        final Ref<Type> rt = new Ref();
        final Boolean b = type.accept(this, rt);
        if (!b.booleanValue())
            return null;

        final ClassOrInterfaceType t = (ClassOrInterfaceType)rt.data;
        final SymbolTable scopeSymbolTable = (SymbolTable)type.getData();

        final ImportDeclaration importDeclaration = scopeSymbolTable.getImport(t.getName());
        if (importDeclaration != null) {
            String name = importDeclaration.getName().getName();
            Ref<Type> arg = new Ref();
            Boolean importResult = tryImportType(name, arg, importDeclaration, scopeSymbolTable);
            if (!importResult.booleanValue()) {
                messages.addWarning1(
                        "Unable to import type '"+t+"'. Performing passthru.", sourcePath,
                        n.getBeginLine(), n.getBeginColumn());
                return null;
            }

            final Node node = scopeSymbolTable.getNode(t.getName());
            // If import is successful then there must be a node for the name.
            assert(node != null);
            return (SymbolTable)node.getData();
        }

        final EnumDeclaration enum_ = scopeSymbolTable.getEnum(t.getName());
        if (enum_ != null) {
            return (SymbolTable)enum_.getData();
        }

        final ClassOrInterfaceDeclaration class_ = scopeSymbolTable.getClass(t.getName());
        if (class_ != null) {
            return (SymbolTable)class_.getData();
        }

        final Node node = scopeSymbolTable.getNode(t.getName());
        if (node != null) {
            System.out.println("node="+node);
            return (SymbolTable)node.getData();
        }

        // Could not find the type. Let the java compiler deal with this
        // situation - the name may be in compiled class file or a jar.

        messages.addWarning1(
                "Unable to find type '"+t+"'. Performing passthru.", sourcePath,
                n.getBeginLine(), n.getBeginColumn());
        return null;
    }

    /**
     * Performs checks related to method calls.
     * @param n - AST node.
     * @param arg - reference to Ref<Type>. This will contain the return type of
     *  called method.
     * @return Boolean.FALSE means that arg.data has not been modified.
     */
    @Override
    public Boolean visit(MethodCallExpr n, Ref<Type> arg) {
        assert(arg != null);
        assert(n.getData() != null && n.getData() instanceof SymbolTable);

        final Ref<Type> refType = arg;

        if (n.getTypeArgs() != null) {

            // In this project anything related to generics is considered to be
            // correct - let java compiler deal with real problems.
            
            messages.addWarning1(
                    "Generics are not supported. Performing pasthru.", sourcePath,
                    n.getBeginLine(), n.getBeginColumn());
            return Boolean.FALSE;
        }

        SymbolTable methodSymbolTable = null;
        int methodScopeModifiers = 0;
        final Expression methodCallScope = n.getScope();
        if (methodCallScope == null) {
            if (currentMethod != null)
                methodScopeModifiers = currentMethod.getModifiers();
            methodSymbolTable = (SymbolTable)n.getData();
        }
        else {

            // Method call scope can be class, interface, local variable, parameter or field.

            // Resolve which symbol table to use - we need a symbol table of the
            // scope of method call. This means the package name, class,
            // enum or interface symbol table.

            final Ref<Type> refScopeType = new Ref();
            Boolean res = methodCallScope.accept(this, refScopeType);
            if (!res.booleanValue())
                return Boolean.FALSE;

            Type scopeType = refScopeType.data;
            if (scopeType instanceof PrimitiveType
                    || scopeType instanceof VoidType) {

                // There are no methods for primitive types or void.

                messages.addError(
                        "'"+scopeType+"' has no methods.", sourcePath,
                        n.getBeginLine(), n.getBeginColumn());
                return Boolean.FALSE;
            }
            
            // Dereference the reference type.
            if (scopeType instanceof ReferenceType) {
                scopeType = dereferenceType(n, (ReferenceType)scopeType);
                if (scopeType == null)
                    return Boolean.FALSE;
            }

            if (scopeType instanceof ClassOrInterfaceType) {

                // Type of method call scope is class or interface. Get the type information of
                // this class or interface, import it if necessary.

                methodSymbolTable = getSymbolTable(n, (ClassOrInterfaceType)scopeType);
                if (methodSymbolTable == null)
                    return Boolean.FALSE;

                methodScopeModifiers = scopeType.getModifiers();
            }
            else {

                // If this assert triggers, I may have missed something.
                assert(false);

                Exception ex;
                try { throw new Exception("Internal error"); } catch (Exception e) { ex = e; }
                messages.addError(ex.toString(), "", 0, 0);
                return Boolean.FALSE;
            }
        }

        assert(methodSymbolTable != null);

        final int argumentCount = (n.getArgs() == null)
                ? 0
                : n.getArgs().size();

        final List<MethodDeclaration> methods = methodSymbolTable.getMethod(n.getName(), argumentCount);
        assert(methods != null); // getMethod should never return null

        if (methods.isEmpty()) {
            // TODO: siin võib olla vajadus lihtsalt läbi lasta see viga, et
            // java kompilaator ise tuvastaks, kas tegemist on veaga või mitte.
            // põhjus selles, et meetod võib tulla kuskilt kompileeritud java
            // klassist või jar-ist.
            messages.addWarning1(
                    "Method '"+n.getName()+"' not found in scope. Passthru.", sourcePath,
                    n.getBeginLine(), n.getBeginColumn());
            return Boolean.FALSE;
        }

        MethodDeclaration theMethod = null;
        if (methods.size() == 1) {
            theMethod = methods.get(0);
        }
        else if (methods.size() > 1) {

            // Determine the argument types.
            List<Type> argTypes = new LinkedList();
            if (argumentCount > 0) {
                int i = 0;
                for (Expression argument : n.getArgs()) {
                    final Ref<Type> argType = new Ref();
                    Boolean res = argument.accept(this, argType);
                    if (!res.booleanValue()) {
                        messages.addWarning1(
                                "Unable to determine type for argument '"+i+"'. Passthru.", sourcePath,
                                n.getBeginLine(), n.getBeginColumn());
                        return Boolean.FALSE;
                    }
                    argTypes.add(argType.data);
                    ++i;
                }
            }
            assert(argTypes.size() == argumentCount);

            // Find a method declaration that best matches the argument types.
            for (final MethodDeclaration m : methods) {
                final List<Parameter> params = m.getParameters();
                boolean isMatch = true;
                for (int i = 0; i < params.size(); ++i) {

                    // 'null' matches any type except for primitive types.
                    if (argTypes.get(i) == null && !(params.get(i).getType() instanceof PrimitiveType))
                        continue;

                    // If argument is not 'null', check if its type matches
                    // that of parameter.
                    if (params.get(i).getType().equals(argTypes.get(i)))
                        continue;

                    isMatch = false;
                    break;
                } // for param

                if (isMatch) {
                    if (theMethod != null) {
                        messages.addWarning1(
                                "Method call '"+n.getName()+"' is ambiguous.", sourcePath,
                                n.getBeginLine(), n.getBeginColumn());
                        // TODO: list all methods that cause ambiguity
                        return Boolean.FALSE;
                    }
                    theMethod = m;
                }
            } // for methods

            if (theMethod == null) {
                messages.addWarning1(
                        "Could not find method '"+n.getName()+"' even with argument dependent lookup.", sourcePath,
                        n.getBeginLine(), n.getBeginColumn());
                // TODO: list all methods that cause ambiguity
                return Boolean.FALSE;
            }
        }

        if (ModifierSet.isReadOnly(methodScopeModifiers)
                && !ModifierSet.isReadOnly(theMethod.getModifiers())
                && !ModifierSet.isStatic(theMethod.getModifiers())) {

            messages.addError(
                    "'"+n.getName()+"' is non-static and non-readonly.", sourcePath,
                    n.getBeginLine(), n.getBeginColumn());
            return Boolean.FALSE;
        }

        if (ModifierSet.isStatic(methodScopeModifiers)
                && !ModifierSet.isStatic(theMethod.getModifiers())) {
            messages.addError(
                    "'"+n.getName()+"': calling non-static method from static context.", sourcePath,
                    n.getBeginLine(), n.getBeginColumn());
            return Boolean.FALSE;
        }

        refType.data = theMethod.getType();
        
        return Boolean.TRUE;
    }

//    private MethodDeclaration findMethodFromBaseclass(MethodDeclaration n, List<ClassOrInterfaceType> bases) {
//
//        for (ClassOrInterfaceType base : bases) {
//        }
//
//        return null;
//    }

    /**
     * Check if covariant return type rules hold.
     * @param n method declaration to check the rules for.
     * @param baseTypes list of class or interface types that the current class or
     * interface is derived from.
     * @return true if the rules hold, false if the rules are broken.
     */
    private boolean checkCovariantReturnType(MethodDeclaration n, List<ClassOrInterfaceType> baseTypes) {

        // If there are no base types, then there can be no covariant return type violations.
        if (baseTypes == null || baseTypes.isEmpty())
            return true;

        assert(currentClassOrInterface != null);
        assert(n.getData() instanceof SymbolTable);

        final SymbolTable symbolTable = (SymbolTable)n.getData();
        final Type returnType = n.getType();
        final boolean methodReturnsReadonly = ModifierSet.isReadOnly(returnType.getModifiers());

        final List<Type> parameterTypes = new LinkedList();
        if (n.getParameters() != null) {
            for (Parameter p : n.getParameters()) {
                parameterTypes.add(p.getType());
            }
        }

        for (ClassOrInterfaceType baseType : baseTypes) {

            final ImportDeclaration import_ = symbolTable.getImport(baseType.getName());
            assert(import_ == null); // should have been taken care of in class declaration.

            final ClassOrInterfaceDeclaration baseDeclaration = symbolTable.getClass(baseType.getName());
            final SymbolTable baseSymbols = (SymbolTable)baseDeclaration.getData();

            final MethodDeclaration baseMethod = baseSymbols.getMethod(n.getName(), parameterTypes);
            if (baseMethod == null) {
                // TODO: dig deeper!
                if (!checkCovariantReturnType(n, baseDeclaration.getExtends()))
                    return false;

                if (!checkCovariantReturnType(n, baseDeclaration.getImplements()))
                    return false;
                
                //messages.addWarning4("TODO: couldn't find method in direct base class. Try indirect base classes!", sourcePath,
                //        n.getBeginLine(), n.getBeginColumn());
            }
            else {
                final Type baseReturnType = baseMethod.getType();
                final boolean baseMethodReturnsReadonly = ModifierSet.isReadOnly(baseReturnType.getModifiers());
                if (methodReturnsReadonly && !baseMethodReturnsReadonly) {
                    // TODO: specify base class/interface that the override violates.
                    messages.addError(
                            "Invalid covariant return type. Cannot convert from '"+returnType+"' to '"+baseReturnType+"' without losing 'readonly'.",
                            sourcePath, n.getBeginLine(), n.getBeginColumn());
                    return false;
                }
            }
        }

        return true;
    }

    /**
     * Changes current method declaration scope. Determines whether the method
     * is an override and if the return type is compatible with return type of
     * the overridden method in the base class.
     * If the method is generic, then no processing and Boolean.FALSE is
     * returned.
     * @post currentMethod reference is the same as it was before calling
     *  this method.
     * @param n AST node of method declaration.
     * @param arg ignored.
     * @return result of visiting the method body.
     */
    @Override
    public Boolean visit(MethodDeclaration n, Ref<Type> arg) {

        if (n.getTypeParameters() != null) {
            messages.addWarning2("Generics not supported.", sourcePath,
                    n.getBeginLine(), n.getBeginColumn());
            return Boolean.FALSE;
        }

        assert(currentClassOrInterface != null);

        if (!checkCovariantReturnType(n, currentClassOrInterface.getExtends()))
            return Boolean.FALSE;

        if (!checkCovariantReturnType(n, currentClassOrInterface.getImplements()))
            return Boolean.FALSE;

        Boolean res = Boolean.TRUE;
        if (n.getBody() != null) {
            final ConstructorDeclaration prevConstructor = currentConstructor;
            final DestructorDeclaration prevDestructor = currentDestructor;
            final MethodDeclaration prevMethod = currentMethod;
            
            currentConstructor = null;
            currentDestructor = null;
            currentMethod = n;
            
            res = n.getBody().accept(this, arg);

            currentConstructor = prevConstructor;
            currentDestructor = prevDestructor;
            currentMethod = prevMethod;
        }
        return res;
    }

    /**
     * Import type specified by import declaration.
     * @param n
     * @param arg
     * @param import_ import declaration.
     * @param symbolTable -
     * @return Boolean.TRUE if type was imported.
     */
    private Boolean tryImportType(String name, Ref<Type> arg, ImportDeclaration import_, SymbolTable symbolTable) {
        assert(import_ != null);
        // This should be taken care of in earlier stage.
        assert(!import_.isAsterisk());

        String importFile = import_.getName().toString().replace(".", File.separator) + ".jpm";
        String importPath = sourceRoot + importFile;
        try {
            final File file = new File(importPath);
            final CompilationUnit cu = JavaParser.parse(file);
            final SymbolTableCreationVisitor stcv = new SymbolTableCreationVisitor(
                    sourceRoot, sourcePath, symbolTable.global());
            stcv.visit(cu, null);

            final SyntaxCheckVisitor scv = new SyntaxCheckVisitor();
            scv.visit(cu, null);

            messages.add(scv.getMessages());
            if (scv.getMessages().hasErrors()) {
                return Boolean.FALSE;
            }

            //final PrivateSymbolStripVisitor pssv = new PrivateSymbolStripVisitor();
            //pssv.visit(cu, null);

            final PackageDeclaration pkg = cu.getPackage();
            if (pkg == null) {
                messages.addError(
                        "No package declaration found in imported file.", importFile,
                        import_.getBeginLine(), import_.getBeginColumn());
                return Boolean.FALSE;
            }

            final NameExpr importedPackageName = pkg.getName();
            final NameExpr importPackageName = ((QualifiedNameExpr)import_.getName()).getQualifier();

            if (!importedPackageName.equals(importPackageName)) {
                messages.addError(
                        "Expected '"+importPackageName+"', but found '"+importedPackageName+"' instead.",
                        importFile,
                        pkg.getBeginLine(), pkg.getBeginColumn());
                return Boolean.FALSE;
            }
            
            //System.err.println("Imported package: " + pkg);

            // TODO: check that imported package is the same as in import declaration.
            // if not then
            //      messages.addError("Bad package name in imported file.");
            //      return FALSE;

            final SymbolTable importedPkgSymbols = (SymbolTable)pkg.getData();
            final TypeDeclaration td = importedPkgSymbols.getType(name);
            if (td == null) {
                messages.addWarning1(
                        "Could not find type '"+name+"' from imported file.", sourcePath,
                        import_.getBeginLine(), import_.getBeginColumn());
                return Boolean.FALSE;
            }
            if (symbolTable.replace(name, import_, td))
                return Boolean.TRUE;
        }
        catch (IOException ex) {
            messages.addError(ex.getMessage(), sourcePath, 0, 0);
            return Boolean.FALSE;
        }
        catch (ParseException ex) {
            messages.addError("Parse error: " + ex.getMessage(), sourcePath, 0, 0);
            return Boolean.FALSE;
        }

        return Boolean.FALSE;
    }

    private Boolean visitInMethod(NameExpr n, Ref<Type> arg) {
        assert(currentMethod != null
                || currentDestructor != null
                || currentConstructor != null);

        assert(arg != null);
        assert(n.getData() != null && n.getData() instanceof SymbolTable);

        final SymbolTable symbolTable = (SymbolTable)n.getData();

        int modifiers = 0;
        Type type = null;

        final ImportDeclaration import_ = symbolTable.getImport(n.getName());
        if (import_ != null) {
            boolean debug = false;
            if (debug)
                System.err.println("Import declaration: " + import_);

            // Import type and replace ImportDeclaration with imported class declaration
            final Boolean importResult = tryImportType(n.getName(), arg, import_, symbolTable);
            if (importResult.booleanValue())
                return visitInMethod(n, arg);

            return Boolean.FALSE;
        }

        final ClassOrInterfaceDeclaration cls = symbolTable.getClass(n.getName());
        if (cls != null) {
            final ClassOrInterfaceType clsType = new ClassOrInterfaceType();
            clsType.setData(cls.getData());
            clsType.setName(cls.getName());
            type = clsType;
        }
        
        if (type == null) {
            final EnumDeclaration enum_ = symbolTable.getEnum(n.getName());
            if (enum_ != null) {
                final ClassOrInterfaceType enumType = new ClassOrInterfaceType();
                enumType.setData(enum_.getData());
                enumType.setName(enum_.getName());
                type = enumType;
            }
        }

        if (type == null) {
            final FieldDeclaration field = symbolTable.getField(n.getName());
            if (field != null) {
                modifiers = field.getModifiers();

                if (!ModifierSet.isStatic(modifiers)) {

                    // In a 'readonly' method, all fields appear as if they
                    // were declared 'final'. 'auto' fields are 'final'
                    // anyway they will get 'readonly'.
                    if (currentMethod != null && ModifierSet.isReadOnly(currentMethod.getModifiers())) {
                        modifiers = ModifierSet.addModifier(modifiers, ModifierSet.FINAL);

                        if (ModifierSet.isAuto(modifiers))
                            modifiers = ModifierSet.addModifier(modifiers, ModifierSet.READONLY);
                    }
                }
                type = field.getType();
            }
        }

        if (type == null) {
            final VariableDeclarationExpr varDecl = symbolTable.getVariable(n.getName());
            if (varDecl != null) {
                modifiers = varDecl.getModifiers();
                type = varDecl.getType();
            }
        }

        if (type == null) {
            final Parameter param = symbolTable.getParameter(n.getName());
            if (param != null) {
                modifiers = param.getModifiers();
                type = param.getType();
            }
        }

        if (type != null) {
            Ref<Type> r = arg;
            r.data = type.clone();
            if (ModifierSet.isReadOnly(modifiers)
                    && type instanceof PrimitiveType) {
                modifiers = ModifierSet.addModifier(modifiers, ModifierSet.FINAL);
            }
            r.data.setModifiers(modifiers);
        }

        if (type == null) {
            // could not find local variable, field nor parameter with
            // given name
            messages.addWarning1(
                "Unable to find symbol '"+n.getName()+"'.", sourcePath,
                n.getBeginLine(), n.getBeginColumn());
            return Boolean.FALSE;
        }

        return Boolean.TRUE;
    }

    /**
     * NameExpr visitor in class scope.
     * @param n
     * @param arg
     * @return
     */
    private Boolean visitInClass(NameExpr n, Ref<Type> arg) {
        assert(currentClassOrInterface != null);
        assert(arg != null);
        assert(n.getData() != null && n.getData() instanceof SymbolTable);

        final SymbolTable symbolTable = (SymbolTable)n.getData();
        final List<Node> symbols = symbolTable.getList(n.getName());

        int modifiers = 0;
        Type type = null;
        if (symbols != null) {
            for (Node symbol : symbols) {
                // In class scope NameExpr may refer to:
                // * field declaration
                // * class or interface declaration
                // * enum declaration
                // * package name
                // * import declaration
                if (symbol instanceof FieldDeclaration) {
                    final FieldDeclaration field = (FieldDeclaration)symbol;
                    modifiers = field.getModifiers();
                    type = field.getType();

                    if (type != null) {
                        Ref<Type> r = arg;
                        r.data = type.clone();
                        if (ModifierSet.isReadOnly(modifiers)
                                && type instanceof PrimitiveType) {
                            modifiers = ModifierSet.addModifier(modifiers, ModifierSet.FINAL);
                        }
                        r.data.setModifiers(modifiers);
                        break;
                    }
                }
                else if (symbol instanceof ImportDeclaration) {
                    boolean debug = false;
                    if (debug)
                        System.err.println("Import declaration: " + symbol);

                    final ImportDeclaration import_ = (ImportDeclaration)symbol;
                    // Import type and replace ImportDeclaration with imported class declaration
                    final Boolean importResult = tryImportType(n.getName(), arg, import_, symbolTable);
                    if (importResult.booleanValue())
                        return visitInClass(n, arg);
                    
                    return Boolean.FALSE;
                }
                else if (symbol instanceof EnumDeclaration) {
                    //System.err.println("Enum declaration: " + symbol);
                    EnumDeclaration enumDecl = (EnumDeclaration)symbol;
                    ReferenceType rt = new ReferenceType();
                    type = arg.data = rt;
                    ClassOrInterfaceType clsType = new ClassOrInterfaceType(enumDecl.getName());
                    clsType.setData(symbol.getData());
                    rt.setType(clsType);
                    rt.setArrayCount(0);
                    rt.setData(symbol.getData());
                }
                else if (symbol instanceof ClassOrInterfaceDeclaration) {
                    //System.err.println("Class or interface declaration: " + symbol);
                    ClassOrInterfaceDeclaration clsDecl = (ClassOrInterfaceDeclaration)symbol;
                    ReferenceType rt = new ReferenceType();
                    type = arg.data = rt;
                    ClassOrInterfaceType clsType = new ClassOrInterfaceType(clsDecl.getName());
                    clsType.setData(symbol.getData());
                    rt.setType(clsType);
                    rt.setArrayCount(0);
                    rt.setData(symbol.getData());
                }
                else {
                    //System.err.println("Unknown declaration: " + symbol);
                    messages.addError("Internal error", sourcePath, 0, 0);
                    return Boolean.FALSE;
                }
            }
        }

        if (type == null) {
            // could not find local variable, field nor parameter with
            // given name, or class, or ..
            messages.addWarning1(
                "Unable to find symbol '"+n.getName()+"' in scope.", sourcePath,
                n.getBeginLine(), n.getBeginColumn());
            return Boolean.FALSE;
        }

        return Boolean.TRUE;
    }

    /**
     * NameExpr visitor. Visitor for NameExpr directly calls this and visitor for QualifiedNameExpr
     * first visits the qualifier and then calls this visitor.
     * @param n
     * @param arg
     * @return
     */
    private Boolean visitNameExpr(NameExpr n, Ref<Type> arg) {
        assert(n.getData() != null);
        assert(n.getData() instanceof SymbolTable);

        if (currentMethod != null
                || currentConstructor != null
                || currentDestructor != null) {
            assert(arg != null);
            return visitInMethod(n, arg);
        }

        if (currentClassOrInterface != null) {
            assert(arg != null);
            return visitInClass(n, arg);
        }

        // it may be a package import, package declaration or something else.

        return Boolean.TRUE;
    }

    /**
     * Check if a variable, parameter or class field exists.
     * @param n name to check for.
     * @param arg - reference to Ref<Type>.
     */
    @Override
    public Boolean visit(NameExpr n, Ref<Type> arg) {
        return visitNameExpr(n, arg);
    }

    @Override
    public Boolean visit(NullLiteralExpr n, Ref<Type> arg) {
        super.visit(n, arg);
        return Boolean.TRUE;
    }

    @Override
    public Boolean visit(ObjectCreationExpr n, Ref<Type> arg) {
        assert(arg != null);
        Ref<Type> refType = arg;
        refType.data = n.getType().clone();
        super.visit(n, arg);
        return Boolean.TRUE;
    }

    @Override
    public Boolean visit(QualifiedNameExpr n, Ref<Type> arg) {
        Ref<Type> qualifierType = new Ref();
        Boolean res = n.getQualifier().accept(this, qualifierType);
        if (res.booleanValue())
            res = visitNameExpr(n, arg);
        
        return res;
    }

    /**
     * Check that returning from method does not violate any readonly rules.
     * @param n
     * @param arg
     * @return
     */
    @Override
    public Boolean visit(ReturnStmt n, Ref<Type> arg) {
        if (n.getExpr() == null)
            return Boolean.FALSE;

        assert(currentMethod != null);
        assert(arg != null);

        final Ref<Type> refType = arg;
        Boolean res = n.getExpr().accept(this, refType);
        if (!res.booleanValue())
            return Boolean.FALSE;

        final Type returnType = currentMethod.getType();
        final Type valueType = refType.data;

        if (returnType instanceof PrimitiveType) {
            if (!(valueType instanceof PrimitiveType)) {
                messages.addError(
                        "Can't convert '"+valueType+"' to '"+returnType+"'.", sourcePath,
                        n.getBeginLine(), n.getBeginColumn());
                return Boolean.FALSE;
            }

            return Boolean.TRUE;
        }

        if (!ModifierSet.isReadOnly(returnType.getModifiers())
                && ModifierSet.isReadOnly(valueType.getModifiers())) {
                messages.addError(
                        "Conversion loses 'readonly'.", sourcePath,
                        n.getBeginLine(), n.getBeginColumn());
                return Boolean.FALSE;
        }

        return Boolean.TRUE;
    }

    @Override
    public Boolean visit(StringLiteralExpr n, Ref<Type> arg) {
        assert(arg != null);
        Ref<Type> refType = arg;
        ClassOrInterfaceType stringType = new ClassOrInterfaceType("String");
        ClassOrInterfaceType lang = new ClassOrInterfaceType("lang");
        ClassOrInterfaceType java = new ClassOrInterfaceType("java");
        lang.setScope(java);
        stringType.setScope(lang);
        refType.data = new ReferenceType(stringType, 0);
        return Boolean.TRUE;
    }

    @Override
    public Boolean visit(SuperExpr n, Ref<Type> arg) {
        if (n.getClassExpr() != null) {
            return n.getClassExpr().accept(this, arg);
        }

        assert(arg != null);
        assert(currentClassOrInterface != null);
        assert(!currentClassOrInterface.isInterface());

        List<ClassOrInterfaceType> extends_ = currentClassOrInterface.getExtends();
        if (extends_ == null) {
            arg.data = new ClassOrInterfaceType("java.lang.Object");
        }
        else {
            assert(!extends_.isEmpty());
            arg.data = extends_.get(0);
        }

        return Boolean.TRUE;
    }

    /**
     * Visits the switch entry statements ignorins switch case labels - for now
     * the Java compiler is better at dealing with it.
     * @param n AST node for switch entry.
     * @param arg -
     * @return Boolean.TRUE.
     */
    @Override
    public Boolean visit(SwitchEntryStmt n, Ref<Type> arg) {

        if (n.getStmts() != null) {
            for (Statement s : n.getStmts()) {
                s.accept(this, arg);
            }
        }
        return Boolean.TRUE;
    }

    /**
     * @pre requires currentClassOrInterface to be not null.
     * @param n AST node for 'this' expression.
     * @param arg - reference to Ref<Type>. Will contain type of 'this' - the
     *  class that word 'this' is used in.
     * @return Boolean.TRUE.
     */
    @Override
    public Boolean visit(ThisExpr n, Ref<Type> arg) {
        if (n.getClassExpr() != null) {
            return n.getClassExpr().accept(this, arg);
        }

        assert(currentClassOrInterface != null);
        assert(arg != null);
        final String name = currentClassOrInterface.getName();
        final SymbolTable symbolTable = (SymbolTable)n.getData();
        ClassOrInterfaceDeclaration clazz = symbolTable.getClass(name);
        assert(clazz != null);

        arg.data = new ClassOrInterfaceType(n.getBeginLine(), n.getBeginColumn(), n.getEndLine(), n.getEndColumn(), null, name, null);
        arg.data.setData(clazz.getData());

        if (currentMethod != null) {
            if (ModifierSet.isStatic(currentMethod.getModifiers())) {
                messages.addError(
                        "There is no 'this' in static methods.", sourcePath,
                        n.getBeginLine(), n.getBeginColumn());
                return Boolean.FALSE;
            }

            if (ModifierSet.isReadOnly(currentMethod.getModifiers())) {
                int modifiers = arg.data.getModifiers();

                modifiers = ModifierSet.addModifier(modifiers, ModifierSet.READONLY);
                arg.data.setModifiers(modifiers);
            }
        }

        super.visit(n, arg);
        return Boolean.TRUE;
    }

    @Override
    public Boolean visit(VariableDeclarationExpr n, Ref<Type> arg) {
        final VariableDeclarationExpr prevVariableDeclaration = currentVariableDeclaration;
        currentVariableDeclaration = n;

        Boolean res = Boolean.TRUE;
        if (n.getAnnotations() != null) {
            for (AnnotationExpr a : n.getAnnotations()) {
                res = a.accept(this, arg);
                if (!res.booleanValue())
                    break;
            }
        }

        if (res.booleanValue()) {
            res = n.getType().accept(this, arg);
            int modifiers = n.getType().getModifiers();
            
            if (ModifierSet.isReadOnly(modifiers) && n.getType() instanceof PrimitiveType) {
                modifiers = ModifierSet.addModifier(modifiers, ModifierSet.FINAL);
                n.getType().setModifiers(modifiers);
            }
        }

        if (res.booleanValue()) {
            for (VariableDeclarator v : n.getVars()) {
                res = v.accept(this, arg);
                if (!res.booleanValue())
                    break;
            }
        }

        currentVariableDeclaration = prevVariableDeclaration;

        return res;
    }

    @Override
    public Boolean visit(VariableDeclarator n, Ref<Type> arg) {
        final Expression init = n.getInit();
        final VariableDeclaratorId id = n.getId();
        Type targetType = null;
        int targetModifiers = 0;

        assert((currentVariableDeclaration == null) != (currentFieldDeclaration == null));

        if (currentVariableDeclaration != null) {
            targetType = currentVariableDeclaration.getType();
            targetModifiers = currentVariableDeclaration.getModifiers();

            if (ModifierSet.isAuto(targetModifiers)) {
                if (init == null) {
                    messages.addError(
                            "Local 'auto' variable must be initialized.", sourcePath,
                            n.getBeginLine(), n.getBeginColumn());
                    return Boolean.FALSE;
                }

                if (!(init instanceof ObjectCreationExpr)) {
                    messages.addError(
                            "'auto' variable must be initialized with new object.", sourcePath,
                            init.getBeginLine(), init.getBeginColumn());
                    return Boolean.FALSE;
                }
            }
        }
        else if (currentFieldDeclaration != null) {
            targetType = currentFieldDeclaration.getType();
            targetModifiers = currentFieldDeclaration.getModifiers();

            if (ModifierSet.isAuto(targetModifiers) && init != null && !(init instanceof ObjectCreationExpr)) {
                messages.addError(
                        "'auto' field must be initialized with new object.", sourcePath,
                        n.getBeginLine(), n.getBeginColumn());
                return Boolean.FALSE;
            }
        }

        if (init == null)
            return Boolean.TRUE;

        Ref<Type> refType = new Ref();
        if (!init.accept(this, refType).booleanValue())
            return Boolean.FALSE;

        if (refType.data == null) {
            if (init instanceof NullLiteralExpr) {
                if (targetType instanceof PrimitiveType) {
                    messages.addError(
                            "Cannot convert 'null' to '"+targetType+"'.", sourcePath,
                            n.getBeginLine(), n.getBeginColumn());
                    return Boolean.FALSE;
                }
                return Boolean.TRUE;
            }
            else {
                messages.addError(
                        "Unable to determine initializer type", sourcePath,
                        n.getBeginLine(), n.getBeginColumn());
                return Boolean.FALSE;
            }
        }

        if (targetType instanceof PrimitiveType)
            return Boolean.TRUE;

        if (!ModifierSet.isReadOnly(targetModifiers)
                && ModifierSet.isReadOnly(refType.data.getModifiers())) {
            
            messages.addError(
                    "Conversion loses 'readonly'", sourcePath,
                    n.getBeginLine(), n.getBeginColumn());
            return Boolean.FALSE;
        }

        return Boolean.TRUE;
    }

    ///////////////////////////////////////////////////////////////////////////

    @Override
    public Boolean visit(AnnotationDeclaration n, Ref<Type> arg) {
        super.visit(n, arg);
        return Boolean.TRUE;
    }

    @Override
    public Boolean visit(AnnotationMemberDeclaration n, Ref<Type> arg) {
        super.visit(n, arg);
        return Boolean.TRUE;
    }

    @Override
    public Boolean visit(ArrayAccessExpr n, Ref<Type> arg) {
        super.visit(n, arg);
        return Boolean.TRUE;
    }

    @Override
    public Boolean visit(ArrayCreationExpr n, Ref<Type> arg) {
        super.visit(n, arg);
        return Boolean.TRUE;
    }

    @Override
    public Boolean visit(ArrayInitializerExpr n, Ref<Type> arg) {
        super.visit(n, arg);
        return Boolean.TRUE;
    }

    @Override
    public Boolean visit(AssertStmt n, Ref<Type> arg) {
        super.visit(n, arg);
        return Boolean.TRUE;
    }

    @Override
    public Boolean visit(BinaryExpr n, Ref<Type> arg) {
        super.visit(n, arg);
        return Boolean.TRUE;
    }

    @Override
    public Boolean visit(BreakStmt n, Ref<Type> arg) {
        super.visit(n, arg);
        return Boolean.TRUE;
    }

    @Override
    public Boolean visit(CatchClause n, Ref<Type> arg) {
        super.visit(n, arg);
        return Boolean.TRUE;
    }

    @Override
    public Boolean visit(ClassExpr n, Ref<Type> arg) {
        super.visit(n, arg);
        return Boolean.TRUE;
    }

    @Override
    public Boolean visit(ConditionalExpr n, Ref<Type> arg) {
        super.visit(n, arg);
        return Boolean.TRUE;
    }

    @Override
    public Boolean visit(ContinueStmt n, Ref<Type> arg) {
        super.visit(n, arg);
        return Boolean.TRUE;
    }

    @Override
    public Boolean visit(DoStmt n, Ref<Type> arg) {
        super.visit(n, arg);
        return Boolean.TRUE;
    }

    @Override
    public Boolean visit(EmptyMemberDeclaration n, Ref<Type> arg) {
        super.visit(n, arg);
        return Boolean.TRUE;
    }

    @Override
    public Boolean visit(EmptyStmt n, Ref<Type> arg) {
        super.visit(n, arg);
        return Boolean.TRUE;
    }

    @Override
    public Boolean visit(EmptyTypeDeclaration n, Ref<Type> arg) {
        super.visit(n, arg);
        return Boolean.TRUE;
    }

    @Override
    public Boolean visit(EnclosedExpr n, Ref<Type> arg) {
        super.visit(n, arg);
        return Boolean.TRUE;
    }

    @Override
    public Boolean visit(EnumConstantDeclaration n, Ref<Type> arg) {
        super.visit(n, arg);
        return Boolean.TRUE;
    }

    @Override
    public Boolean visit(EnumDeclaration n, Ref<Type> arg) {
        super.visit(n, arg);
        return Boolean.TRUE;
    }

    @Override
    public Boolean visit(ExplicitConstructorInvocationStmt n, Ref<Type> arg) {
        super.visit(n, arg);
        return Boolean.TRUE;
    }

    @Override
    public Boolean visit(ExpressionStmt n, Ref<Type> arg) {
        super.visit(n, arg);
        return Boolean.TRUE;
    }

    @Override
    public Boolean visit(ForeachStmt n, Ref<Type> arg) {
        super.visit(n, arg);
        return Boolean.TRUE;
    }

    @Override
    public Boolean visit(ForStmt n, Ref<Type> arg) {
        super.visit(n, arg);
        return Boolean.TRUE;
    }

    @Override
    public Boolean visit(IfStmt n, Ref<Type> arg) {
        super.visit(n, arg);
        return Boolean.TRUE;
    }

    @Override
    public Boolean visit(ImportDeclaration n, Ref<Type> arg) {
        // find file containing the class
        // parse the contents - only public symbols are required.
        super.visit(n, arg);
        return Boolean.TRUE;
    }

    @Override
    public Boolean visit(InitializerDeclaration n, Ref<Type> arg) {
        super.visit(n, arg);
        return Boolean.TRUE;
    }

    @Override
    public Boolean visit(InstanceOfExpr n, Ref<Type> arg) {
        super.visit(n, arg);
        return Boolean.TRUE;
    }

    @Override
    public Boolean visit(IntegerLiteralMinValueExpr n, Ref<Type> arg) {
        super.visit(n, arg);
        return Boolean.TRUE;
    }

    @Override
    public Boolean visit(JavadocComment n, Ref<Type> arg) {
        super.visit(n, arg);
        return Boolean.TRUE;
    }

    @Override
    public Boolean visit(LabeledStmt n, Ref<Type> arg) {
        super.visit(n, arg);
        return Boolean.TRUE;
    }

    @Override
    public Boolean visit(LongLiteralMinValueExpr n, Ref<Type> arg) {
        super.visit(n, arg);
        return Boolean.TRUE;
    }

    @Override
    public Boolean visit(MarkerAnnotationExpr n, Ref<Type> arg) {
        super.visit(n, arg);
        return Boolean.TRUE;
    }

    @Override
    public Boolean visit(MemberValuePair n, Ref<Type> arg) {
        super.visit(n, arg);
        return Boolean.TRUE;
    }

    @Override
    public Boolean visit(NormalAnnotationExpr n, Ref<Type> arg) {
        super.visit(n, arg);
        return Boolean.TRUE;
    }

    @Override
    public Boolean visit(PackageDeclaration n, Ref<Type> arg) {
        super.visit(n, arg);
        return Boolean.TRUE;
    }

    @Override
    public Boolean visit(Parameter n, Ref<Type> arg) {
        super.visit(n, arg);
        return Boolean.TRUE;
    }

    @Override
    public Boolean visit(PrimitiveType n, Ref<Type> arg) {
        super.visit(n, arg);
        return Boolean.TRUE;
    }

    @Override
    public Boolean visit(ReferenceType n, Ref<Type> arg) {
        super.visit(n, arg);
        return Boolean.TRUE;
    }

    @Override
    public Boolean visit(SingleMemberAnnotationExpr n, Ref<Type> arg) {
        super.visit(n, arg);
        return Boolean.TRUE;
    }

    @Override
    public Boolean visit(SwitchStmt n, Ref<Type> arg) {
        super.visit(n, arg);
        return Boolean.TRUE;
    }

    @Override
    public Boolean visit(SynchronizedStmt n, Ref<Type> arg) {
        super.visit(n, arg);
        return Boolean.TRUE;
    }

    @Override
    public Boolean visit(ThrowStmt n, Ref<Type> arg) {
        super.visit(n, arg);
        return Boolean.TRUE;
    }

    @Override
    public Boolean visit(TryStmt n, Ref<Type> arg) {
        super.visit(n, arg);
        return Boolean.TRUE;
    }

    @Override
    public Boolean visit(TypeDeclarationStmt n, Ref<Type> arg) {
        super.visit(n, arg);
        return Boolean.TRUE;
    }

    @Override
    public Boolean visit(TypeParameter n, Ref<Type> arg) {
        super.visit(n, arg);
        return Boolean.TRUE;
    }

    @Override
    public Boolean visit(UnaryExpr n, Ref<Type> arg) {
        super.visit(n, arg);
        return Boolean.TRUE;
    }

    @Override
    public Boolean visit(VariableDeclaratorId n, Ref<Type> arg) {
        super.visit(n, arg);
        return Boolean.TRUE;
    }

    @Override
    public Boolean visit(VoidType n, Ref<Type> arg) {
        super.visit(n, arg);
        return Boolean.TRUE;
    }

    @Override
    public Boolean visit(WhileStmt n, Ref<Type> arg) {
        super.visit(n, arg);
        return Boolean.TRUE;
    }

    @Override
    public Boolean visit(WildcardType n, Ref<Type> arg) {
        super.visit(n, arg);
        return Boolean.TRUE;
    }

    @Override
    public Boolean visit(BlockComment n, Ref<Type> arg) {
        super.visit(n, arg);
        return Boolean.TRUE;
    }

    @Override
    public Boolean visit(LineComment n, Ref<Type> arg) {
        super.visit(n, arg);
        return Boolean.TRUE;
    }
}
